package org.viettan.android.main.services;

import java.io.IOException;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import org.viettan.android.main.R;
import org.viettan.android.main.VietTanApplication;
import org.viettan.android.main.activities.radio.RadioEpisodeReorderActivity;
import org.viettan.android.main.io.Logger;
import org.viettan.android.main.model.RadioEpisode;
import org.viettan.android.main.view.ViewUtility;

import roboguice.service.RoboService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.net.Uri;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.RemoteException;

import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Singleton;

/**
 * The {@code RadioService} is a background service that runs under its own process and plays back
 * {@link RadioEpisode}s in the backround. Even if the main {@link VietTanApplication} is closed,
 * this service will continue running until the episode finished playing.
 */
@Singleton
public class RadioService extends RoboService implements OnPreparedListener, OnCompletionListener {
    private static final long UPDATE_INTERVAL_MS = 1000L;

    @Inject
    private Logger logger;

    private NotificationManager notificationManager;
    private MediaPlayer mediaPlayer = null;
    private volatile Timer updateProgressTimer;
    private final List<RadioProgressListener> listeners = Lists.newArrayList();
    private final IRadioService.Stub apiEndPoint = new ApiEndPoint();

    @Override
    public void onCreate() {
	super.onCreate();

	notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
	logger.info("RadioService created");
    }

    @Override
    public IBinder onBind(Intent intent) {
	if (RadioService.class.getName().equals(intent.getAction())) {
	    return apiEndPoint;
	}
	return null;
    }

    /**
     * Request to start playing a new {@link RadioEpisode}.
     *
     * @param radioEpisode
     * @throws RemoteException
     */
    public synchronized void play(RadioEpisode radioEpisode) throws RemoteException {
	Preconditions.checkArgument(radioEpisode != null, "RadioEpisode cannot be null");

	// stopping current playback of previous play
	if (mediaPlayer != null) {
	    stop();
	}

	initializeMediaPlayer();
	Uri uri = Uri.parse(radioEpisode.getMp3Url().toString());
	if (uri == null) {
	    logger.warn("Unable to parse URL for radio service");
	    return;
	}

	try {
	    logger.info("About to set source");
	    mediaPlayer.setDataSource(getApplicationContext(), uri);
	    mediaPlayer.prepareAsync();
	    showNotification(radioEpisode.getTitle(), radioEpisode.getSubtitle());
	} catch (IllegalArgumentException e) {
	    throw new RemoteException();
	} catch (IllegalStateException e) {
	    throw new RemoteException();
	} catch (IOException e) {
	    throw new RemoteException();
	}
    }

    /**
     * Enough data has been buffered and we can commence playing back the episode.
     */
    public void onPrepared(MediaPlayer mediaPlayer) {
	Preconditions.checkArgument(mediaPlayer != null, "MediaPlayer cannot be null");

	mediaPlayer.start();
	logger.info("MediaPlayer started");
	startProgressUpdates();
    }

    /**
     * Request to resume a paused episode.
     */
    protected synchronized void resume() {
	Preconditions.checkState(mediaPlayer != null, "No MediaPlayer to resume");

	mediaPlayer.start();
	startProgressUpdates();
	logger.info("MediaPlayer resumed");
    }

    /**
     * Pause playing an episode, but remember its time location so that it can be resumed.
     */
    public synchronized void pause() {
	Preconditions.checkState(mediaPlayer != null, "No MediaPlayer to pause");

	mediaPlayer.pause();
	stopProgressUpdates();
	logger.info("MediaPlayer paused");
    }

    /**
     * Seeks playback to a specific position in the media file.
     *
     * @param milliSeconds
     *            the position in milliseconds to seek to
     */
    public synchronized void seekTo(int milliSeconds) {
	if (mediaPlayer == null) {
	    return;
	}

	mediaPlayer.seekTo(milliSeconds);
	logger.info("MediaPlayer seeked to " + ViewUtility.getTime(milliSeconds));
    }

    /**
     * Callback when the radio episode has completed playing to the end. At that point we just want
     * to stop and close down this view.
     */
    public void onCompletion(MediaPlayer ignored) {
	// we don't want to be call stop() here ourselves, but we send a message back
	// to the RadioManager/RadioEpisodeActivity so the UI has a change of finishing and
	// going back to the previous activity in the stack.
	synchronized (listeners) {
	    for (RadioProgressListener listener : listeners) {
		try {
		    logger.warn("Playback completed");
		    listener.onPlaybackCompleted();
		} catch (RemoteException e) {
		    logger.warn("Unable update listeners about playback completion");
		}
	    }
	}
    };

    /**
     * Stops playing the current episode. This will also stop and destroy the service to release
     * memory.
     */
    public synchronized void stop() {
	if (mediaPlayer == null) {
	    return;
	}

	stopProgressUpdates();
	mediaPlayer.stop();
	mediaPlayer.release();
	mediaPlayer = null;
	logger.info("MediaPlayer stopped and released");
    }

    private synchronized void initializeMediaPlayer() {
	Preconditions.checkState(mediaPlayer == null, "Previous MediaPlayer leaked");

	mediaPlayer = new MediaPlayer();
	mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
	mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
	mediaPlayer.setOnPreparedListener(this);
	mediaPlayer.setOnCompletionListener(this);
	logger.info("MediaPlayer initialized");
    }

    private synchronized void startProgressUpdates() {
	if (updateProgressTimer != null) {
	    return;
	}

	updateProgressTimer = new Timer(RadioService.class.getSimpleName());
	updateProgressTimer.schedule(new UpdateProgressTask(), UPDATE_INTERVAL_MS,
		UPDATE_INTERVAL_MS);
    }

    private synchronized void stopProgressUpdates() {
	if (updateProgressTimer == null) {
	    return;
	}

	updateProgressTimer.cancel();
	updateProgressTimer = null;
    }

    /**
     * Shows the notification while this service is running.
     */
    private void showNotification(String title, String subtitle) {
	Notification notification = new Notification(R.drawable.viettanapp, title,
		System.currentTimeMillis());

	// note: a bit of hack to bring RadioEpisodeActvity to the front. See
	// https://groups.google.com/forum/?fromgroups#!msg/android-developers/DibTfwnfZ-s/5HrfVnoKAaMJ
	Intent radioEpisodeIntent = new Intent(this, RadioEpisodeReorderActivity.class);
	radioEpisodeIntent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
	PendingIntent radioEpisodePendingIntent = PendingIntent.getActivity(this, 0,
		radioEpisodeIntent, 0);

	notification.setLatestEventInfo(this, title, subtitle, radioEpisodePendingIntent);
	notificationManager.notify(R.string.radio_ctm, notification);
    }

    @Override
    public void onDestroy() {
	super.onDestroy();

	notificationManager.cancel(R.string.radio_ctm);
	logger.info("RadioService destroyed");
    }

    /**
     * This is the API end point that proxies commands over to the service.
     */
    private class ApiEndPoint extends IRadioService.Stub {
	public void play(RadioEpisode radioEpisode) throws RemoteException {
	    RadioService.this.play(radioEpisode);
	}

	public void pause() throws RemoteException {
	    RadioService.this.pause();
	}

	public void resume() throws RemoteException {
	    RadioService.this.resume();
	}

	public void seekTo(int milliSeconds) throws RemoteException {
	    RadioService.this.seekTo(milliSeconds);
	}

	public void stop() throws RemoteException {
	    RadioService.this.stop();
	}

	public void addListener(RadioProgressListener listener) throws RemoteException {
	    Preconditions.checkArgument(listener != null, "Listener cannot be null");
	    synchronized (listeners) {
		listeners.add(listener);
	    }
	}

	public void removeListener(RadioProgressListener listener) throws RemoteException {
	    Preconditions.checkArgument(listener != null, "Listener cannot be null");
	    synchronized (listeners) {
		listeners.remove(listener);
	    }
	}
    };

    private class UpdateProgressTask extends TimerTask {
	@Override
	public void run() {
	    if (!mediaPlayer.isPlaying()) {
		return;
	    }
	    int currentPosition = mediaPlayer.getCurrentPosition();
	    int maxPosition = mediaPlayer.getDuration();

	    synchronized (listeners) {
		for (RadioProgressListener listener : listeners) {
		    try {
			listener.onProgressUpdate(currentPosition, maxPosition);
		    } catch (RemoteException e) {
			logger.warn("Unable update listeners about current position");
		    }
		}
	    }
	}
    }
}
